// Author: wsfuyibing <682805@qq.com>
// Date: 2025-04-10

package src

import (
	"context"
	"strings"
	"xorm.io/xorm"
)

type (
	// Dao is an alias of Database Access Object, It's read and write data on
	// database server such as mysql, postgres.
	Dao[T any] struct {
		sess *xorm.Session
		key  string
		typ  T
	}

	DaoFieldNameForPrimaryKey interface {
		PrimaryKeyName() string
	}

	DaoFieldNameForPrimaryKeyValue interface {
		PrimaryKeyValue() any
	}
)

// NewDao creates a dao instance with given model type and model struct. Data
// in database operated depends on model type.
//
//	type ModelService struct {
//	    Dao *db.Dao[models.Model]
//	}
//
//	func NewModelService(dbs ...*framework.DB) *ModelService {
//	    o := &ModelService{}
//	    o.Dao = db.NewDao[models.ErpSales](models.ErpSales{})
//	    o.Dao.WithConn(dbs...)
//	    o.Dao.WithKey("db")
//	    return o
//	}
func NewDao[T any](t T) *Dao[T] {
	return &Dao[T]{
		typ: t,
		key: DefaultKey,
	}
}

// +---------------------------------------------------------------------------+
// | Common operations                                                         |
// +---------------------------------------------------------------------------+

// Master returns a session from master connection.
func (o *Dao[T]) Master(ctx context.Context) (*xorm.Session, error) {
	if o.sess != nil {
		return o.sess.Context(ctx), nil
	}
	return Config.GetSlave(ctx, o.key)
}

// Slaver returns a session from slaver connection.
func (o *Dao[T]) Slaver(ctx context.Context) (*xorm.Session, error) {
	if o.sess != nil {
		return o.sess.Context(ctx), nil
	}
	return Config.GetSlave(ctx, o.key)
}

// WithSess bind a session of database connection.
func (o *Dao[T]) WithSess(ss ...*xorm.Session) *Dao[T] {
	if len(ss) > 0 && ss[0] != nil {
		o.sess = ss[0]
	}
	return o
}

// WithKey bind a connection key for connector.
func (o *Dao[T]) WithKey(k string) *Dao[T] {
	if k != "" {
		o.key = k
	}
	return o
}

// +---------------------------------------------------------------------------+
// | Read / Get one record                                                     |
// +---------------------------------------------------------------------------+

func (o *Dao[T]) CountByMap(ctx context.Context, condition map[string]any) (total int64, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Send query on database.
	model := &o.typ
	total, err = sess.Where(condition).Count(model)
	return
}

// GetBy returns a model specified column and value.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetBy(ctx, "id", 10)
func (o *Dao[T]) GetBy(ctx context.Context, column string, value any) (model *T, has bool, err error) {
	return o.GetByMap(ctx, map[string]any{
		column: value,
	})
}

// GetById returns a model by given primary key value.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetById(ctx, 10)
func (o *Dao[T]) GetById(ctx context.Context, value any) (model *T, has bool, err error) {
	return o.GetByMap(ctx, map[string]any{
		o.parsePrimaryKeyName(): value,
	})
}

// GetByMap returns a model by given condition with mapper param.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetByMap(ctx, map[string]any{
//	    "id": 10
//	})
func (o *Dao[T]) GetByMap(ctx context.Context, condition map[string]any) (model *T, has bool, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Init an empty model.
	model = &o.typ

	// Send query on database.
	if has, err = sess.Where(condition).Get(model); err != nil || !has {
		model = nil
	}
	return
}

// GetByStruct returns a model with model condition. It's ignore default value
// condition such as zero or empty string.
//
//	// sql: SELECT * FROM `table` WHERE `id` = 10 LIMIT 1
//	svc.Dao.GetByModel(ctx, &models.Model{
//	    Id: 10,
//	})
func (o *Dao[T]) GetByStruct(ctx context.Context, t T) (model *T, has bool, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Init an empty model.
	model = &o.typ

	// Send query on database.
	if has, err = sess.Where(t).Get(model); err != nil || !has {
		model = nil
	}
	return
}

// +---------------------------------------------------------------------------+
// | Read / List records.                                                      |
// +---------------------------------------------------------------------------+

// ListByMap returns a list of models by given condition with map param.
//
//	sql: SELECT * FROM `table` WHERE `user_id` = 1
//	svc.Dao.GetByModel(ctx, map[string]any{
//	    "user_id": 1
//	})
func (o *Dao[T]) ListByMap(ctx context.Context, m map[string]any, sorts ...string) (list []*T, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Init an empty model list.
	list = make([]*T, 0)

	// Send query on database.
	if len(sorts) > 0 {
		err = sess.Where(m).OrderBy(strings.Join(sorts, ",")).Find(&list)
	} else {
		err = sess.Where(m).Find(&list)
	}
	if err != nil {
		list = nil
	}
	return
}

// ListByStruct returns a list of models by given condition with model struct.
//
//	// sql: SELECT * FROM `table` WHERE `user_id` = 10
//	svc.Dao.GetByModel(ctx, &models.Model{
//	    UserId: 1
//	})
func (o *Dao[T]) ListByStruct(ctx context.Context, t *T, sorts ...string) (list []*T, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Init an empty model list.
	list = make([]*T, 0)

	// Send query on database.
	if len(sorts) > 0 {
		err = sess.OrderBy(strings.Join(sorts, ",")).Find(&list, t)
	} else {
		err = sess.Find(&list, t)
	}
	if err != nil {
		list = nil
	}
	return
}

// +---------------------------------------------------------------------------+
// | Read / List records with paginator.                                       |
// +---------------------------------------------------------------------------+

// PagingByMap returns a list of models by given condition with map and calculate
// total items in table.
func (o *Dao[T]) PagingByMap(ctx context.Context, m map[string]any, page, size int, sorts ...string) (list []*T, total int64, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Execute matched record count.
	if total, err = sess.Table(o.typ).Where(m).Count(); err != nil {
		return
	}

	// Return if no matched record.
	if total == 0 {
		return
	}

	// Init an empty model list.
	list = make([]*T, 0)

	// Send query on database.
	if len(sorts) > 0 {
		err = sess.Where(m).OrderBy(strings.Join(sorts, ",")).Limit(size, (page-1)*size).Find(&list)
	} else {
		err = sess.Where(m).Limit(size, (page-1)*size).Find(&list)
	}
	if err != nil {
		list = nil
	}
	return
}

// PagingByStruct returns a list of models by given condition with model struct and
// calculate total items in table.
func (o *Dao[T]) PagingByStruct(ctx context.Context, t *T, page, size int, sorts ...string) (list []*T, total int64, err error) {
	var sess *xorm.Session

	// Open a session from slaver connection.
	if sess, err = o.Slaver(ctx); err != nil {
		return
	}

	// Execute matched record count.
	if total, err = sess.Count(t); err != nil {
		return
	}

	// Return if no matched record.
	if total == 0 {
		return
	}

	// Init an empty model list.
	list = make([]*T, 0)

	// Send query on database.
	if len(sorts) > 0 {
		err = sess.OrderBy(strings.Join(sorts, ",")).Limit(size, (page-1)*size).Find(&list, t)
	} else {
		err = sess.Limit(size, (page-1)*size).Find(&list, t)
	}
	if err != nil {
		list = nil
	}
	return
}

// +---------------------------------------------------------------------------+
// | Write / Add new record                                                    |
// +---------------------------------------------------------------------------+

// AddByStruct adds a model by given model struct.
//
//	svc.Dao.AddByModel(ctx, &models.Model{
//	    UserId: 1,
//	    ...
//	})
func (o *Dao[T]) AddByStruct(ctx context.Context, t *T) (model *T, err error) {
	var sess *xorm.Session

	// Open a session from master connection.
	if sess, err = o.Master(ctx); err != nil {
		return
	}

	// Send query on database.
	if _, err = sess.Insert(t); err == nil {
		model = t
	}
	return
}

// +---------------------------------------------------------------------------+
// | Write / Delete records                                                    |
// +---------------------------------------------------------------------------+

// DeleteBy delete records by given condition with key value pairs. Param k is
// column name and v is column value.
//
//	// sql: DELETE FORM `table` WHERE `id` = 1
//	svc.Dao.DeleteBy(ctx, "id", 1)
func (o *Dao[T]) DeleteBy(ctx context.Context, k string, v any) (affects int64, err error) {
	return o.DeleteByMap(ctx, map[string]any{k: v})
}

// DeleteById delete records by given condition with primary key value. Param v is
// an integer value of primary key.
//
//	// sql: DELETE FORM `table` WHERE `id` = 1
//	svc.Dao.DeleteById(ctx, 1)
func (o *Dao[T]) DeleteById(ctx context.Context, v any) (affects int64, err error) {
	return o.DeleteByMap(ctx, map[string]any{o.parsePrimaryKeyName(): v})
}

// DeleteByMap delete records by given condition with multiple key value pairs.
//
//	// sql: DELETE FORM `table` WHERE `status` = 0 AND `status_type` = "error"
//	svc.Dao.DeleteByMap(ctx, map[string]any{
//	    "status": 0,
//	    "status_type": "error",
//	})
func (o *Dao[T]) DeleteByMap(ctx context.Context, m map[string]any) (affects int64, err error) {
	var sess *xorm.Session

	// Open a session from master connection.
	if sess, err = o.Master(ctx); err != nil {
		return
	}

	// Send delete request.
	affects, err = sess.Where(m).Delete(o.typ)
	return
}

// DeleteByStruct delete records by given condition with struct definition.
//
//	// sql: DELETE FORM `table` WHERE `status` = 1 AND `status_type` = "error"
//	svc.Dao.DeleteByMap(ctx, models.Model{
//	    "status": 1,
//	    "status_type": "error",
//	})
func (o *Dao[T]) DeleteByStruct(ctx context.Context, t *T) (affects int64, err error) {
	var sess *xorm.Session

	// Open a session from master connection.
	if sess, err = o.Master(ctx); err != nil {
		return
	}

	// Send delete query on database.
	affects, err = sess.Delete(t)
	return
}

// +---------------------------------------------------------------------------+
// | Write                                                                     |
// +---------------------------------------------------------------------------+

// UpdateFieldsById updates a record with key value pairs by primary key.
//
//	// sql: UPDATE `table` SET `name` = "test", `age` = 18 WHERE `id` = 10
//	svc.Dao.UpdateMapById(ctx, map[string]any{
//	    "name": "test",
//	    "age": 18,
//	}, 10)
func (o *Dao[T]) UpdateFieldsById(ctx context.Context, fields map[string]any, v any) (affects int64, err error) {
	return o.UpdateFieldsByMap(ctx, fields, map[string]any{
		o.parsePrimaryKeyName(): v,
	})
}

// UpdateFieldsByMap updates a record with map condition.
//
//	// sql: UPDATE `table` SET `name` = "test", `age` = 18 WHERE `id` = 10 AND `status` = 0
//	svc.Dao.UpdateFieldsByMap(ctx, map[string]any{
//	    "name": "test",
//	    "age": 18,
//	}, map[string]any{
//	    "id": 10,
//	    "status": 0,
//	})
func (o *Dao[T]) UpdateFieldsByMap(ctx context.Context, fields, condition map[string]any) (affects int64, err error) {
	var sess *xorm.Session

	// Open a session from master connection.
	if sess, err = o.Master(ctx); err != nil {
		return
	}

	// Send update query on database.
	affects, err = sess.Table(o.typ).Where(condition).Update(fields)
	return
}

func (o *Dao[T]) UpdateModel(ctx context.Context, model *T) (affects int64, err error) {
	var sess *xorm.Session

	// Open a session from master connection.
	if sess, err = o.Master(ctx); err != nil {
		return
	}

	// Send update query on database.
	affects, err = sess.Where(map[string]any{
		o.parsePrimaryKeyName(): o.parsePrimaryKeyValue(),
	}).Update(model)

	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *Dao[T]) parsePrimaryKeyName() string {
	var ptr any = &o.typ
	if v, ok := ptr.(DaoFieldNameForPrimaryKey); ok {
		return v.PrimaryKeyName()
	}
	return "id"
}

func (o *Dao[T]) parsePrimaryKeyValue() any {
	var ptr any = &o.typ
	if v, ok := ptr.(DaoFieldNameForPrimaryKeyValue); ok {
		return v.PrimaryKeyValue()
	}
	return 0
}
